4.5 Any?可空类型(Nullable Types)

可空类型是Kotlin类型系统的一个特性,主要是为了解决Java中的令人头疼的 NullPointerException 问题。

我们知道,在Java中如果一个变量可以是null,来那么使用它调用一个方法就是不安全的,因为它会导致:NullPointerException

Kotlin把可空性(nullability)作为类型系统的一部分,Kotlin编译器可以直接在编译过程中发现许多可能的错误,并减少在运行时抛出异常的可能性。

Kotlin的类型系统和Java相比,首要的区别就是Kotlin对可空类型的显式支持。

在本节中,我们将讨论Kotlin中的可空类型。

4.5.1 null 是什么

对于Java程序员来说,null是令人头痛的东西。我们时常会受到空指针异常(NPE)的骚扰。就连Java的发明者都承认这是他的一项巨大失误。Java为什么要保留null呢?null出现有一段时间了,并且我认为Java发明者知道null与它解决的问题相比带来了更多的麻烦,但是null仍然陪伴着Java。

我们通常把null理解为编程语言中定义特殊的0, 把我们初始化的指针指向它,以防止“野指针”的恶果。在Java中,null是任何引用类型的默认值,不严格的说是所有Object类型的默认值。

这里的null既不是对象也不是一种类型,它仅是一种特殊的值,我们可以将其赋予任何引用类型,也可以将null转化成任何类型。在编译和运行时期,将null强制转换成任何引用类型都是可行的,在运行时期都不会抛出空指针异常。注意,这里指的是任何Java的引用类型。在遇到基本类型int long float double short byte 等的时候,情况就不一样了。而且还是个坑。编译器不会报错,但是运行时会抛NPE。空指针异常。这是Java中的自动拆箱导致的。代码示例:

  1. Integer nullInt = null; // this is ok
  2. int anotherInt = nullInt; // 编译器允许这么赋值, 但是在运行时抛 NullPointerException

所以,我们写Java代码的时候,要时刻注意这一点:Integer的默认值是null而不是0。当把null值传递给一个int型变量的时候,Java的自动装箱将会返回空指针异常。

4.5.2 Kotlin 中的 null

在Kotlin中,针对Java中的null的杂乱局面,进行了整顿,作了清晰的界定,并在编译器级别强制规范了可空null变量类型的使用。

我们来看一下Kotlin中关于null的一些有趣的运算。

nullnull是相等的:

  1. >>> null==null
  2. true
  3. >>> null!=null
  4. false

null这个值比较特殊,null 不是Any类型

  1. >>> null is Any
  2. false

但是,nullAny?类型:

  1. >>> null is Any?
  2. true

我们来看看null对应的类型到底是什么:

  1. >>> var a=null
  2. >>> a
  3. null
  4. >>> a=1
  5. error: the integer literal does not conform to the expected type Nothing?
  6. a=1
  7. ^

从报错信息我们可以看出,null的类型是Nothing?。关于Nothing?我们将会在下一小节中介绍。

我们可以对null进行加法运算:

  1. >>> "1"+null
  2. 1null
  3. >>> null+20
  4. null20

对应的重载运算符的函数定义在kotlin/Library.kt里面:

  1. package kotlin
  2. import kotlin.internal.PureReifiable
  3. /**
  4. * Returns a string representation of the object. Can be called with a null receiver, in which case
  5. * it returns the string "null".
  6. */
  7. public fun Any?.toString(): String
  8. /**
  9. * Concatenates this string with the string representation of the given [other] object. If either the receiver
  10. * or the [other] object are null, they are represented as the string "null".
  11. */
  12. public operator fun String?.plus(other: Any?): String
  13. ...

但是,反过来就不行了:

  1. >>> 1+null
  2. error: none of the following functions can be called with the arguments supplied:
  3. public final operator fun plus(other: Byte): Int defined in kotlin.Int
  4. public final operator fun plus(other: Double): Double defined in kotlin.Int
  5. public final operator fun plus(other: Float): Float defined in kotlin.Int
  6. public final operator fun plus(other: Int): Int defined in kotlin.Int
  7. public final operator fun plus(other: Long): Long defined in kotlin.Int
  8. public final operator fun plus(other: Short): Int defined in kotlin.Int
  9. 1+null
  10. ^

这是因为Int没有重载传入null参数的plus()函数。

4.5.3 可空类型 String? 与安全调用 ?.

我们来看一个例子。下面是计算字符串长度的简单Java方法:

  1. public static int getLength1(String str) {
  2. return str.length();
  3. }

我们已经习惯了在这样的Java代码中,加上这样的空判断处理:

  1. public static int getLength2(String str) throws Exception {
  2. if (null == str) {
  3. throw new Exception("str is null");
  4. }
  5. return str.length();
  6. }

而在Kotlin中,当我们同样写一个可能为null参数的函数时:

  1. fun getLength1(str: String): Int {
  2. return str.length
  3. }

当我们传入一个null参数时:

  1. @Test fun testGetLength1() {
  2. val StringUtilKt = StringUtilKt()
  3. StringUtilKt.getLength1(null)
  4. }

编译器就直接编译失败:

  1. e: /Users/jack/easykotlin/chapter4_type_system/src/test/kotlin/com/easy/kotlin/StringUtilKtTest.kt: (15, 33): Null can not be a value of a non-null type String
  2. :compileTestKotlin FAILED
  3. FAILURE: Build failed with an exception.
  4. * What went wrong:
  5. Execution failed for task ':compileTestKotlin'.
  6. > Compilation error. See log for more details

如果我们使用IDEA,会在编码时就直接提示错误了:

Kotlin极简教程

这样通过编译时强制排除空指针的错误,大大减少了出现NPE的可能。

另外,如果我们确实需要传入一个可空的参数,我们可以使用可空类型String?来声明一个可以指向空指针的变量。

可空类型可以用来标记任何一个变量,来表明这个变量是可空的(Nullable)。例如:Char?, Int?, MineType?(自定义的类型)等等。

我们用示例代码来更加简洁的说明:

  1. >>> var x:String="x"
  2. >>> x=null
  3. error: null can not be a value of a non-null type String
  4. x=null
  5. ^
  6. >>> var y:String?="y"
  7. >>> y=null
  8. >>> y
  9. null

我们可以看出:普通String类型,是不允许指向null的;而可空String?类可以指向null

下面我们来尝试使用一个可空变量来调用函数:

  1. >>> fun getLength2(str: String?): Int? = str.length
  2. error: only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
  3. fun getLength2(str: String?): Int? = str.length
  4. ^

编译器直接报错,告诉我们,变量str: String?是可空的类型,调用只能通过安全调用?. 或者 非空断言调用!!.

另外,如果不需要捕获异常来处理,我们可以使用Kotlin里面的安全调用符?.

Kotlin极简教程

代码示例:

  1. fun getLength2(str: String?): Int? {
  2. return str?.length
  3. }

测试代码:

  1. @Test fun testGetLength2() {
  2. val StringUtilKt = StringUtilKt()
  3. println(StringUtilKt.getLength2(null)) //null
  4. Assert.assertTrue(3 == StringUtilKt.getLength2("abc"))
  5. }

我们可以看出,当我们使用安全调用?. , 代码安静的执行输出了null

如果,我们确实想写一个出现空指针异常的代码,那就使用可能出现空指针的断言调用符!!.

代码示例:

  1. fun getLength3(str: String?): Int? {
  2. return str!!.length
  3. }

测试代码:

  1. @Test fun testGetLength3() {
  2. val StringUtilKt = StringUtilKt()
  3. println(StringUtilKt.getLength3(null))
  4. Assert.assertTrue(3 == StringUtilKt.getLength3("abc"))
  5. }

上面的代码就跟Java里面差不多了,运行会直接抛出空指针异常:

  1. kotlin.KotlinNullPointerException
  2. at com.easy.kotlin.StringUtilKt.getLength3(StringUtilKt.kt:16)
  3. at com.easy.kotlin.StringUtilKtTest.testGetLength3(StringUtilKtTest.kt:28)

这里的KotlinNullPointerException 是KotlinNullPointerException.java代码,继承了Java中的java.lang.NullPointerException, 它的源代码如下:

  1. package kotlin;
  2. public class KotlinNullPointerException extends NullPointerException {
  3. public KotlinNullPointerException() {
  4. }
  5. public KotlinNullPointerException(String message) {
  6. super(message);
  7. }
  8. }

另外,如果异常需要捕获到进行特殊处理的场景,在Kotlin中仍然使用 try ... catch 捕获并处理异常。

4.5.4 可空性的实现原理

我们来看一段Kotlin的可空类型的示例代码如下:

  1. fun testNullable1(x: String, y: String?): Int {
  2. return x.length
  3. }
  4. fun testNullable2(x: String, y: String?): Int? {
  5. return y?.length
  6. }
  7. fun testNullable3(x: String, y: String?): Int? {
  8. return y!!.length
  9. }

我们来使用IDEA的Kotlin插件来看下可空类型的安全调用的等价Java代码。

打开IDEA的 Tools > Kotlin > Show Kotlin Bytecode

Kotlin极简教程

然后,点击Decompile , 我们可以得到反编译的Java代码

  1. public final class NullableTypesKt {
  2. public static final int testNullable1(@NotNull String x, @Nullable String y) {
  3. Intrinsics.checkParameterIsNotNull(x, "x");
  4. return x.length();
  5. }
  6. @Nullable
  7. public static final Integer testNullable2(@NotNull String x, @Nullable String y) {
  8. Intrinsics.checkParameterIsNotNull(x, "x");
  9. return y != null?Integer.valueOf(y.length()):null;
  10. }
  11. @Nullable
  12. public static final Integer testNullable3(@NotNull String x, @Nullable String y) {
  13. Intrinsics.checkParameterIsNotNull(x, "x");
  14. if(y == null) {
  15. Intrinsics.throwNpe();
  16. }
  17. return Integer.valueOf(y.length());
  18. }
  19. }

在不可空变量调用函数之前,都检查了是否为空, 使用的是kotlin.jvm.internal.Intrinsics这个Java类里面的checkParameterIsNotNull方法。如果是null就抛出异常:

  1. public static void checkParameterIsNotNull(Object value, String paramName) {
  2. if (value == null) {
  3. throwParameterIsNullException(paramName);
  4. }
  5. }

同时,我们可以看出在Kotlin中函数的入参声明

  1. fun testNullable(x: String, y: String?)

反编译成等价的Java代码是

  1. public static final void testNullable(@NotNull String x, @Nullable String y)

我们可以看出,这里使用注解@NotNull标注不可空的变量,使用注解@Nullable标注一个变量可空。

可空变量的安全调用符y?.length 等价的Java代码就是:

  1. y != null?Integer.valueOf(y.length()):null

可空变量的断言调用y!!.length等价的Java代码是:

  1. if(y == null) {
  2. Intrinsics.throwNpe();
  3. }
  4. return Integer.valueOf(y.length());

4.5.5 可空类型层次体系

就像Any是在非空类型层次结构的根,
Any?是可空类型层次的根。
由于Any?是Any的超集,所以,Any?是Kotlin的类型层次结构的最顶端。

Kotlin极简教程

代码示例:

  1. >>> 1 is Any
  2. true
  3. >>> 1 is Any?
  4. true
  5. >>> null is Any
  6. false
  7. >>> null is Any?
  8. true
  9. >>> Any() is Any?
  10. true